package it.antonio.ilpizze.db.core;

import static it.antonio.ilpizze.db.io.IOConstants.SIZE_BOOLEAN;
import static it.antonio.ilpizze.db.io.IOConstants.SIZE_BYTE;
import static it.antonio.ilpizze.db.io.IOConstants.SIZE_DOUBLE;
import static it.antonio.ilpizze.db.io.IOConstants.SIZE_FLOAT;
import static it.antonio.ilpizze.db.io.IOConstants.SIZE_INT;
import static it.antonio.ilpizze.db.io.IOConstants.SIZE_LONG;
import it.antonio.ilpizze.db.DbException;
import it.antonio.ilpizze.db.Id;
import it.antonio.ilpizze.db.core.id.FilePositionId;
import it.antonio.ilpizze.db.core.id.RecordFilePositionId;
import it.antonio.ilpizze.db.io.IOConstants;
import it.antonio.ilpizze.db.io.IOFile;

import java.lang.reflect.Array;

public enum Types {
	
	NULL{
		@Override
		public byte typeByte() {
			return 0;
		}

		@Override
		public int computeSize(Object obj) {
			return SIZE_BYTE;
		}

		@Override
		public void write(IOFile file, Object obj) {
			byte b = 77;
			file.writeByte(b);
		}

		@Override
		public Object read(IOFile file) {
			byte b = file.readByte();
			if(b == 77) return null;
			throw new DbException("Expected null Value");
		}
		
	},
	STRING{
		@Override
		public byte typeByte() {
			return 1;
		}

		@Override
		public int computeSize(Object obj) {
			return SIZE_INT + ((String) obj).length();
		}

		@Override
		public void write(IOFile file, Object obj) {
			file.writeString((String)obj);
		}

		@Override
		public Object read(IOFile file) {
			return file.readString();
		}
		
	},
	INT{
		@Override
		public byte typeByte() {
			return 2;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_INT;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeInt((Integer) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readInt();
		}
	},
	LONG{
		@Override
		public byte typeByte() {
			return 3;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_LONG;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeLong((Long) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readLong();
		}
	},
	FLOAT{
		@Override
		public byte typeByte() {
			return 4;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_FLOAT;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeFloat((Float) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readFloat();
		}
	},
	DOUBLE{
		@Override
		public byte typeByte() {
			return 5;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_DOUBLE;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeDouble((Double) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readDouble();
		}
	},
	BOOLEAN{
		@Override
		public byte typeByte() {
			return 6;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_BOOLEAN;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeBoolean((Boolean) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readBoolean();
		}
	},
	BYTE{
		@Override
		public byte typeByte() {
			return 7;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_BYTE;
		}
		@Override
		public void write(IOFile file, Object obj) {
			file.writeByte((Byte) obj);
		}
		@Override
		public Object read(IOFile file) {
			return file.readByte();
		}
	},
	ARRAY{
		@Override
		public byte typeByte() {
			return 8;
		}
		@Override
		public int computeSize(Object obj) {
			int length = Array.getLength(obj);
			int size = IOConstants.SIZE_BYTE; // type
			size += IOConstants.SIZE_BOOLEAN; // primitive
			size += IOConstants.SIZE_INT; // arraylength
			for(int i = 0; i < length; i++) {
				Object o = Array.get(obj, i);
				Types type = Types.decode(o);
				size += type.computeSize(o);
			}  
			return size;
		}
		@Override
		public void write(IOFile file, Object obj) {
			Class<?> clazz = obj.getClass().getComponentType();
			
			file.writeByte(innerTypeForCollection(clazz).typeByte());
			file.writeBoolean(clazz.isPrimitive());
			
			int length = Array.getLength(obj);
			file.writeInt(length);
			for(int i = 0; i < length; i++) {
				Object o = Array.get(obj, i);
				Types type = Types.decode(o);
				file.writeByte(type.typeByte());
				type.write(file, o);
			}  
		}
		
		@Override
		public Object read(IOFile file) {
			Types type = Types.decode(file.readByte());
			boolean isPrimitive = file.readBoolean();
			
			int length = file.readInt();
			Object array;
			switch (type) {
				case STRING: array = Array.newInstance(String.class, length); break;
				case LONG: array = isPrimitive ? Array.newInstance(long.class, length) : Array.newInstance(Long.class, length); break;
				case INT: array = isPrimitive ? Array.newInstance(int.class, length) : Array.newInstance(Integer.class, length); break;
				case DOUBLE: array = isPrimitive ? Array.newInstance(double.class, length) : Array.newInstance(Double.class, length); break;
				case FLOAT: array = isPrimitive ? Array.newInstance(float.class, length) : Array.newInstance(Float.class, length); break;
				case BOOLEAN: array = isPrimitive ? Array.newInstance(boolean.class, length) : Array.newInstance(Boolean.class, length); break;
				case BYTE: array = isPrimitive ? Array.newInstance(byte.class, length) : Array.newInstance(Byte.class, length); break;
				case ID: array = Array.newInstance(Id.class, length); break;
				default: throw new DbException("Wrong Type for Array");
			}
			for(int i = 0; i < length; i++){
				Types elementType = Types.decode(file.readByte());
				Array.set(array, i, elementType.read(file));
			}
			return array;
		}
	}, ID{
		@Override
		public byte typeByte() {
			return 9;
		}
		@Override
		public int computeSize(Object obj) {
			return SIZE_LONG;
		}
		@Override
		public void write(IOFile file, Object obj) {
			FilePositionId id = (FilePositionId) obj;
			file.writeLong(id.pos());
		}
		@Override
		public Object read(IOFile file) {
			return RecordFilePositionId.atPosition(file.readLong());
		}
	};
	
	public abstract byte typeByte();
	public abstract int computeSize(Object obj);
	public abstract void write(IOFile file, Object obj);
	public abstract Object read(IOFile file);
	
	public static Types decode(byte b){
		for(Types type: Types.values()){
			if(b == type.typeByte()) return type;
		}
		throw new DbException("type non supported");
	}
	
	public static Types decode(Object obj){
		if(obj == null) return NULL;
		if(obj instanceof String) return STRING;
		if(obj instanceof Integer) return INT;
		if(obj instanceof Long) return LONG;
		if(obj instanceof Float) return FLOAT;
		if(obj instanceof Double) return DOUBLE;
		if(obj instanceof Boolean) return BOOLEAN;
		if(obj instanceof Byte) return BYTE;
		if(obj.getClass().isArray()) return ARRAY;
		if(obj instanceof Id) return ID;
		throw new DbException("Type non supported: " + obj.getClass());
	}
	
	
	public static int size(Object obj){
		return decode(obj).computeSize(obj);
	}
	
	// UTILS
	
	private static Types innerTypeForCollection(Class<?> clazz){
		if(clazz.equals(String.class)) return STRING;
		if(clazz.equals(Integer.class) || clazz.equals(int.class)) return INT;
		if(clazz.equals(Long.class) || clazz.equals(long.class)) return LONG;
		if(clazz.equals(Float.class) || clazz.equals(float.class)) return FLOAT;
		if(clazz.equals(Double.class) || clazz.equals(double.class)) return DOUBLE;
		if(clazz.equals(Boolean.class) || clazz.equals(boolean.class)) return BOOLEAN;
		if(clazz.equals(Byte.class) || clazz.equals(byte.class)) return BYTE;
		if(clazz.equals(Id.class)) return ID;
		throw new DbException("Type non supported for Array: " + clazz);
	}
	
}
